package com.magenta.maxoptra.geocoding.ptv;

import com.magenta.maxoptra.dto.GeoLocationRecord;
import com.magenta.maxoptra.geocoding.ptv.util.XRouteUtils;
import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.VehicleType;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.util.GeoInformationClient;
import com.ptvag.jabba.core.exception.SystemException;
import com.ptvag.jabba.service.baseservices.CallerContext;
import com.ptvag.jabba.service.baseservices.CallerContextProperty;
import com.ptvag.xserver.common.*;
import com.ptvag.xserver.xroute.*;
import com.ptvag.xserver.xroute.LinkType;

import static com.magenta.maxoptra.gis.MapProviderType.ptv;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Created by skolznev on 24.03.14.
 */
public class GeoProviderPtv extends BaseGeocoding<String> implements IGeoRouting {

    private final String XROUTE_WS_URL = "http://192.0.2.37:50030/xroute/ws/XRoute";
    private final int DEFAULT_READ_TIMEOUT = 220000;   //milli seconds
    private final String SERVER_VERSION = "1.16";

    private static Map<String, GeoStatusCode> ptvErrors = new ConcurrentHashMap<String, GeoStatusCode>();

    static {
        ptvErrors.put("1", GeoStatusCode.G_GEO_SUCCESS);
        ptvErrors.put("102", GeoStatusCode.G_GEO_SERVER_ERROR);
        ptvErrors.put("103", GeoStatusCode.G_GEO_UNKNOWN_DIRECTIONS);
    }

    private String getGeocodingUrl() {
        return XROUTE_WS_URL;
    }

    @Override
    public void setClient(GeoInformationClient client) {
        this.setClient(client);
    }

    @Override
    protected boolean accuracyIsAccurateEnough(String accuracyThreshold, String accuracy) {
        return false;
    }

    @Override
    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int i,
                                                      List<String> accuracies) throws GeoException {
        throw new UnsupportedOperationException("method doFindAddressByKeyWord() is not supported for " +
                this.getProviderName());
    }

    @Override
    public GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to) throws GeoException,
            ServerUnavailableException {
        return getRoute(from, to, null);
    }

    @Override
    public GeoRoute getRoute(GeographicCoordinates from, GeographicCoordinates to, VehicleType vehicleType)
            throws GeoException, ServerUnavailableException {

        if (from == null || to == null) {
            return null;
        }
        XRouteRemoteInterface xRouteClient = XRouteUtils
                .makeClient(XROUTE_WS_URL);
        if (xRouteClient != null) {
            return calculateRoute(xRouteClient, from, to);
        }
        return null;
    }


    public GeoRoute calculateRoute(XRouteRemoteInterface xRouteClient, GeographicCoordinates from,
                                   GeographicCoordinates to) throws GeoException {
        GeoRoute geoRoute = null;

        //setting context
        CallerContextProperty ccCoordFormat = new CallerContextProperty();
        ccCoordFormat.setKey("CoordFormat");
        ccCoordFormat.setValue("OG_GEODECIMAL");
        CallerContextProperty ccResponseGeometry = new CallerContextProperty();
        ccResponseGeometry.setKey("ResponseGeometry");
        ccResponseGeometry.setValue("PLAIN");

        CallerContext callerContext = new CallerContext();
        callerContext.setProperties(new CallerContextProperty[]{
                ccCoordFormat, ccResponseGeometry});

        xRouteClient.setCallerContext(callerContext);
        // Step 2.1: define way-points <start> and <destination>
        Point depPoint = new Point();
        depPoint.setPoint(new PlainPoint(from.getLongitude(), from.getLatitude()));
        WaypointDesc wpdStart = new WaypointDesc();
        wpdStart.setCoords(new Point[]{depPoint});
        wpdStart.setLinkType(LinkType.AUTO_LINKING);

        Point destPoint = new Point();
        destPoint.setPoint(new PlainPoint(to.getLongitude(), to.getLatitude()));
        WaypointDesc wpdDest = new WaypointDesc();
        wpdDest.setCoords(new Point[]{destPoint});
        //Define the link type
        wpdDest.setLinkType(LinkType.AUTO_LINKING);

        //Define routing optimization parameters
        RoutingOption[] routingOption = new RoutingOption[]{
                new RoutingOption(RoutingParameter.ROUTE_LANGUAGE, "EN"),
                new RoutingOption(RoutingParameter.OPTIMIZATION, OptimizationParams.SHORTEST_PATH.getValue()),
                new RoutingOption(RoutingParameter.REQUEST_VERSION, SERVER_VERSION),
                new RoutingOption(RoutingParameter.COUNTRY_ENCODING, "ISO2")
        };

        // Set both way-points into the specific array WaypointDesc[]
        WaypointDesc[] waypointDescs = new WaypointDesc[]{wpdStart, wpdDest};

        try {
            // Return result from object routeInfo
            RouteInfo routeInfo = xRouteClient.calculateRouteInfo(waypointDescs, routingOption, null);

            double distance = routeInfo.getDistance() / 1000.0;             //distance in km
            long duration = routeInfo.getTime() * 1000;                // time in milliseconds

            geoRoute = new GeoRoute(distance, duration, from, to);

        } catch (SystemException se) {
            System.out.println("Exception info: " + se.getMessage());
//            logger.error(se.getMessage());
            throw new GeoException(se.toString());
        } catch (XRouteException xre) {
//            logger.error(xre.getMessage());
            throw new GeoException(xre.toString());
        } catch (XServiceException xse) {
//            logger.error(xse.getMessage());
            throw new GeoException(xse.toString());
        }
        return geoRoute;
    }

    //Calculation of matrix 1xN (Default request timeout)
    public List<GeoRoute> calculateMatrix(XRouteRemoteInterface client, GeographicCoordinates from,
                                          List<? extends GeographicCoordinates> to) throws GeoException {
        return calculateMatrix(client, from, to, DEFAULT_READ_TIMEOUT);
    }

    //Calculation of matrix 1xN (Custom request timeout)
    public List<GeoRoute> calculateMatrix(XRouteRemoteInterface xRouteClient, GeographicCoordinates from,
                                          List<? extends GeographicCoordinates> to, int timeout) throws GeoException {

        if (timeout > 0) {
            xRouteClient.setTimeout(timeout);
        }

        CallerContextProperty ccCoordFormat = new CallerContextProperty();
        ccCoordFormat.setKey("CoordFormat");
        ccCoordFormat.setValue("OG_GEODECIMAL");
        CallerContextProperty ccResponseGeometry = new CallerContextProperty();
        ccResponseGeometry.setKey("ResponseGeometry");
        ccResponseGeometry.setValue("PLAIN");

        CallerContext callerContext= new CallerContext();
        callerContext.setProperties(new CallerContextProperty[]{
                ccCoordFormat,ccResponseGeometry});

        xRouteClient.setCallerContext(callerContext);

        RoutingOption[] routingOption = new RoutingOption[]{
                new RoutingOption(RoutingParameter.ROUTE_LANGUAGE, "EN"),
                new RoutingOption(RoutingParameter.OPTIMIZATION, OptimizationParams.SHORTEST_PATH.getValue()),
                new RoutingOption(RoutingParameter.REQUEST_VERSION, SERVER_VERSION),
                new RoutingOption(RoutingParameter.COUNTRY_ENCODING, "ISO2")
        };
        WaypointDesc[] startWPs = makeWaypoints(from, true);
        WaypointDesc[] destWPs = makeWaypoints(to, true);
        List<GeoRoute> results = null;
        try {
            // Call method matrixInfo and return Object matrixInfo.
            MatrixInfo matrixInfo = xRouteClient.calculateMatrixInfo(startWPs,
                    destWPs, routingOption, null);

            double distance = 0;
            long time = 0;
            int i = 0;
            results = new ArrayList<GeoRoute>(to.size());
            for (RouteInfo routeInfo : matrixInfo.getMatrixInfo()) {
                distance = routeInfo.getDistance() / 1000.0;        //distance in km
                time = routeInfo.getTime() * 1000;             //time in milliseconds
                GeoRoute route = new GeoRoute(distance, time, from, to.get(i));
                results.add(route);
            }

        } catch (SystemException se) {
//            logger.error(se.getMessage());
            throw new GeoException(se.toString());
        } catch (XRouteException xre) {
//            logger.error(xre.getMessage());
            throw new GeoException(xre.toString());
        } catch (XServiceException xse) {
//            logger.error(xse.getMessage());
            throw new GeoException(xse.toString());
        }
        return results;
    }


    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from, List<? extends GeographicCoordinates> to,
                                   int timeout) throws GeoException, ServerUnavailableException {
        throw new UnsupportedOperationException("Method getRoute(GeographicCoordinates " +
                "from,List<? extends GeographicCoordina" +
                "tes> to, int timeout)  is not supported for " + getProviderName() + ".");
    }


    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings, GeographicCoordinates from,
                                                  GeographicCoordinates to) throws GeoException {
        if (from == null || to == null) {
            return null;
        }
        XRouteRemoteInterface xRouteClient = XRouteUtils
                .makeClient(XROUTE_WS_URL);
        if (xRouteClient != null) {
            return getPlainPoints(xRouteClient, from, to);
        }
        return null;
    }

    public List<GeoLocationRecord> getPlainPoints(XRouteRemoteInterface xRouteClient, GeographicCoordinates from,
                                                  GeographicCoordinates to){

        List <GeoLocationRecord> pointsList =  new ArrayList<GeoLocationRecord>();

        CallerContextProperty ccCoordFormat = new CallerContextProperty();
        ccCoordFormat.setKey("CoordFormat");
        ccCoordFormat.setValue("OG_GEODECIMAL");
        CallerContextProperty ccResponseGeometry = new CallerContextProperty();
        ccResponseGeometry.setKey("ResponseGeometry");
        ccResponseGeometry.setValue("PLAIN");

        CallerContext callerContext= new CallerContext();
        callerContext.setProperties(new CallerContextProperty[]{
                ccCoordFormat,ccResponseGeometry});

        xRouteClient.setCallerContext(callerContext);

        // define way-points <start> and <destination>
        Point startPoint = new Point();
        startPoint.setPoint(new PlainPoint(from.getLongitude(), from.getLatitude()));
        WaypointDesc wpdStart = new WaypointDesc();
        wpdStart.setCoords(new Point[]{startPoint});
        wpdStart.setLinkType(LinkType.AUTO_LINKING);

        Point destPoint = new Point();
        destPoint.setPoint(new PlainPoint(to.getLongitude(), to.getLatitude()));
        WaypointDesc wpdDest = new WaypointDesc();
        wpdDest.setCoords(new Point[]{destPoint});
        wpdDest.setLinkType(LinkType.AUTO_LINKING);

        // Set both way-points into the specific array WaypointDesc[]
        WaypointDesc[] waypointDescs = new WaypointDesc[]{wpdStart, wpdDest};

        // Set result list options
        ResultListOptions resultListOptions = new ResultListOptions();
        resultListOptions.setTotalRectangle(true);
        resultListOptions.setPolygon(true);
        resultListOptions.setBoundingRectanglesOffset(0);
        resultListOptions.setBoundingRectanglesC(0);
       // It is also important to select the detail level
        resultListOptions.setDetailLevel(DetailLevel.STANDARD);
        Route route = null;

        RoutingOption[] routingOption = new RoutingOption[]{
                new RoutingOption(RoutingParameter.ROUTE_LANGUAGE, "EN") ,
                new RoutingOption(RoutingParameter.OPTIMIZATION,OptimizationParams.SHORTEST_PATH.getValue()),
                new RoutingOption(RoutingParameter.REQUEST_VERSION, "1.16"),
                new RoutingOption(RoutingParameter.COUNTRY_ENCODING,"ISO2")
        };
        try {
            route = xRouteClient.calculateRoute(waypointDescs, routingOption, null, resultListOptions);
            //retrieving points from the result
            PlainPoint plainPoint[] = route.getPolygon().getLineString().getPoints();
            if (plainPoint != null) {
                for (PlainPoint pp : plainPoint) {
                    GeoLocationRecord geoLocationRecord = new GeoLocationRecord(pp.getX(),pp.getY());
                    pointsList.add(geoLocationRecord);
                }
            }
        } catch (SystemException se) {
//            System.err.println("Network or URL problem: " + se);
        } catch (XRouteException xre) {
//            System.err.println("Problem with calculating a route." +  xre.getMessage());
        } catch (XServiceException xse) {
//            System.err.println("Technical problem on server");
        }
        return pointsList;
    }


    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address, int resultAddressLimit)
            throws GeoException {
        throw new UnsupportedOperationException("Method doForwardGeocoding() is not supported for " + getProviderName()
                + ".");
    }

    @Override
    public String getProviderName() {
        return ptv.name();
    }


    @Override
    public List<GeoLocationRecord> getRoutePoints(GeoProviderSettings settings,
                                                  List<GeographicCoordinates> points) throws GeoException {
        throw new UnsupportedOperationException("Method getRoutePoints() is not supported for " + getProviderName() +
                ".");
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from,
                                    List<? extends GeographicCoordinates> to, int timeout)
            throws GeoException, ServerUnavailableException {
        return getRoutes(from, to, null, timeout);
    }

    @Override
    public List<GeoRoute> getRoutes(GeographicCoordinates from,
                                    List<? extends GeographicCoordinates> to, VehicleType vehicleType,
                                    int timeout) throws GeoException, ServerUnavailableException {
        if (vehicleType == null) {
            vehicleType = VehicleType.Default;
        }
        if (from == null) {
            return null;
        }
        if (to.isEmpty()) {
            return Collections.emptyList();
        }

        XRouteRemoteInterface xRouteClient = XRouteUtils
                .makeClient(XROUTE_WS_URL);
        if (xRouteClient != null) {
            return calculateMatrix(xRouteClient, from, to, timeout);
        }
        return null;
    }

    @Override
    public List<GeoRoute> getCoordinateToCoordinateRoutes(List<GeographicCoordinates> coordinates)
            throws ServerUnavailableException {
        throw new UnsupportedOperationException("Method getCoordinateToCoordinateRoutes() from coordinate to list" +
                " of coordinates is not supported for " + getProviderName() + ".");
    }

    @Override
    public List<GeoRoute> getRoute(GeographicCoordinates from,
                                   List<? extends GeographicCoordinates> to, VehicleType vehicleType,
                                   int timeout) throws GeoException, ServerUnavailableException {
        throw new UnsupportedOperationException("Method getRoute(GeographicCoordinates from,\n" +
                "                                   List<? extends GeographicCoordinates> to, VehicleType vehicleType,\n" +
                "                                   int timeout) is not supported for " + getProviderName() + ".");
    }


    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates)
            throws GeoException {
        throw new UnsupportedOperationException("Method getRoute(List<GeographicCoordinates> coordinates) is" +
                " not supported for " + getProviderName() + ".");
    }

    @Override
    public GeoRoute getRoute(List<GeographicCoordinates> coordinates,
                             VehicleType vehicleType) throws GeoException {
        throw new UnsupportedOperationException("Method getRoute(List<GeographicCoordinates> coordinates,\n" +
                "                             VehicleType vehicleType) is not supported for " + getProviderName() + ".");
    }

    public static Map<String, GeoStatusCode> getPtvErrors() {
        return ptvErrors;
    }

    public WaypointDesc[] makeWaypoints(GeographicCoordinates from, boolean isSetLinkType) {
//        double coords[] = Coordinates_Transformation.GeoDecimal_2_Mercator(PTV_MULTIPLY_COEF * from.getLongitude(),
//                PTV_MULTIPLY_COEF * from.getLatitude());
        Point point = new Point();
        point.setPoint(new PlainPoint(from.getLongitude(), from.getLatitude()));
        WaypointDesc wpdStart = new WaypointDesc();
        wpdStart.setCoords(new Point[]{point});
        if (isSetLinkType) {
            wpdStart.setLinkType(LinkType.AUTO_LINKING);
        }
        WaypointDesc waypointDesc[] = new WaypointDesc[]{wpdStart};
        return waypointDesc;
    }

    public WaypointDesc[] makeWaypoints(List<? extends GeographicCoordinates> list, boolean isSetLinkType) {
        WaypointDesc[] result = new WaypointDesc[list.size()];
        for (int i = 0; i < list.size(); i++) {
            Point point = new Point();
//            double coords[] = Coordinates_Transformation.GeoDecimal_2_Mercator(PTV_MULTIPLY_COEF *
//                    list.get(i).getLongitude(),
//                    PTV_MULTIPLY_COEF * list.get(i).getLatitude());
            point.setPoint(new PlainPoint(list.get(i).getLongitude(), list.get(i).getLatitude()));
            WaypointDesc wpdStart = new WaypointDesc();
            wpdStart.setCoords(new Point[]{point});
            if (isSetLinkType) {
                wpdStart.setLinkType(LinkType.AUTO_LINKING);
            }
            result[i] = wpdStart;
        }
        return result;
    }

    public String toString() {
        return "Geoprovider named " + getProviderName();
    }
}
